Skip to content

feat: containerd/nerdctl engine backend#445

Draft
AaronFeledy wants to merge 78 commits intomainfrom
feat/containerd-engine
Draft

feat: containerd/nerdctl engine backend#445
AaronFeledy wants to merge 78 commits intomainfrom
feat/containerd-engine

Conversation

@AaronFeledy
Copy link
Copy Markdown
Member

@AaronFeledy AaronFeledy commented Mar 14, 2026

Summary

Adds containerd + nerdctl + BuildKit as an alternative container engine backend. Lando can run independently of Docker with engine: "containerd" in config.

What

  • Backend abstractionDaemonBackend, ContainerBackend, ComposeBackend interfaces
  • Docker backend — existing code wrapped as implementations, zero behavior changes
  • Containerd backendContainerdDaemon (containerd + buildkitd + finch-daemon lifecycle), ContainerdContainer (nerdctl ops), NerdctlCompose (compose adapter)
  • finch-daemon — Docker API compatibility socket for Traefik proxy
  • Platform support — Linux/WSL2 native, macOS via Lima VM
  • Setup UXlando setup prompts for engine selection, downloads binaries from GitHub
  • Doctor checks — binary/daemon/connectivity health checks
  • Error messages — 9 user-friendly containerd-specific messages
  • Perf tooling — timer utility, benchmark script, debug-mode perf logging

Config

engine: containerd  # or "docker" (default) or "auto"

Stats

72 files changed, ~10,000 lines added. 467 tests passing. No existing behavior changed — Docker path works exactly as before.

Not yet done

  • Leia integration tests do not run against containerd in CI (default is auto → Docker)
  • Needs a dedicated CI job with engine: containerd to exercise the backend end-to-end

Note

High Risk
Introduces a new container engine backend (containerd/nerdctl) and changes engine bootstrap/setup paths, which can impact core lifecycle, orchestration, and proxy behavior across platforms. Risk is mitigated by preserving the existing Docker codepath but the surface area is large and includes new daemon/process management.

Overview
Adds an experimental containerd engine option (and default engine: auto) that lets Lando run via containerd + buildkitd + nerdctl instead of Docker, including engine selection during lando setup and containerd-specific compatibility/health checks.

Replaces utils/setup-engine.js bootstrapping with a new BackendManager that instantiates either the Docker-backed engine or a new containerd-backed engine; the containerd path manages its own daemons, uses nerdctl compose for orchestration, and introduces a Docker-API compatibility socket (finch-daemon) to keep the Traefik proxy working.

Extends CI to run core Leia tests against both docker and containerd, adds engine documentation and developer benchmarking tooling (scripts/benchmark-engines.sh).

Written by Cursor Bugbot for commit 9f53c2d. This will update automatically on new commits. Configure here.

Define explicit base classes (DaemonBackend, ContainerBackend,
ComposeBackend, EngineBackend) that any container engine backend
must implement. Extracted from existing Docker/Dockerode code.

Includes all 14 Engine facade methods, router dispatch documentation,
abstract class guards, and comprehensive JSDoc contracts.

Part of the containerd/nerdctl engine initiative.
Create DockerDaemon, DockerContainer, DockerCompose classes that
implement the backend interfaces by delegating to existing
LandoDaemon, Landerode, and compose.js code.

Uses getter/setter proxying for live property access.
No existing files modified - full backward compatibility.

Part of the containerd/nerdctl engine initiative.
ContainerdDaemon manages Lando's own isolated containerd + buildkitd
instances. Handles lifecycle (up/down/isUp), PID management, socket
health checks, stderr logging, and elevated (sudo) starts with PID
discovery.

Platform support: Linux/WSL native, macOS/Windows stubbed with
helpful errors pending Lima VM integration.

Part of the containerd/nerdctl engine initiative.
ContainerdContainer implements ContainerBackend by shelling out to
nerdctl for all container operations. Includes JSONL parsing,
label normalization (handles commas in values), proxy objects for
getContainer/getNetwork, and the full Lando container filtering
pipeline from Landerode.list().

Part of the containerd/nerdctl engine initiative.
NerdctlCompose extends ComposeBackend by delegating to the existing
compose.js command builder and prepending nerdctl --address <socket>
compose to every command array. Zero duplicated logic — just a thin
transform layer.

Part of the containerd/nerdctl engine initiative.
BackendManager factory creates the right Engine based on config.engine
setting (auto | docker | containerd). Auto-detection prefers containerd
if all binaries exist, falls back to Docker.

New config defaults: engine, containerdBin, nerdctlBin, buildkitdBin,
containerdSocket. All non-breaking (engine defaults to auto, overrides
default to null).

setup-engine-containerd.js provides standalone containerd wiring.
Existing setup-engine.js and lando.js untouched.

Part of the containerd/nerdctl engine initiative.
Utility modules for locating and downloading containerd stack binaries:
- get-containerd-x.js, get-nerdctl-x.js, get-buildkit-x.js (binary resolution)
- get-containerd-download-url.js (GitHub release URL construction)
- setup-containerd-binaries.js (download + install missing binaries)

Follows existing get-docker-x.js patterns. Supports linux/darwin,
amd64/arm64.

Part of the containerd/nerdctl engine initiative.
75 unit tests covering BackendManager, NerdctlCompose, ContainerdContainer
(including parseLabels comma-in-value fix), and download URL generation.
All passing.

Documentation for the new engine config option at docs/config/engine.md.

Part of the containerd/nerdctl engine initiative.
Replace setup-engine.js call with BackendManager.createEngine() in
bootstrapEngine(). Engine selection now driven by config.engine
setting (auto | docker | containerd).

Old setup-engine.js call kept as commented reference.
BackendManager exposed as lando.backendManager for plugins.

Part of the containerd/nerdctl engine initiative.
Setup hook downloads containerd, buildkitd, and nerdctl from GitHub
releases during 'lando setup'. Skips when engine=docker.
Check hook warns when engine=containerd but binaries are missing.

Part of the containerd/nerdctl engine initiative.
LimaManager class handles Lima VM lifecycle (create/start/stop/exec)
for running containerd on macOS. ContainerdDaemon now creates and
manages a Lima VM on darwin instead of throwing 'not implemented'.

Exposes containerd socket at ~/.lima/lando/sock/containerd.sock.

Part of the containerd/nerdctl engine initiative.
Engine.getCompatibility() now handles both Docker and containerd
version formats. Adds supportedContainerdVersions config,
engineBackend property, and containerd-aware dockerInstalled/
composeInstalled logic.

Part of the containerd/nerdctl engine initiative.
Wire lando-setup-containerd-engine into pre-setup event and
lando-setup-containerd-engine-check into pre-engine-autostart.

Part of the containerd/nerdctl engine initiative.
31 test cases covering BackendManager, ContainerdDaemon lifecycle,
ContainerdContainer operations, NerdctlCompose command generation,
and full engine lifecycle. Tests requiring real containerd auto-skip.

Part of the containerd/nerdctl engine initiative.
app-check-containerd-compat.js validates containerd/nerdctl/buildkit
versions and reports warnings. lando-get-containerd-compat.js runs
Engine.getCompatibility() for containerd backends.

Part of the containerd/nerdctl engine initiative.
WslHelper handles WSL-specific concerns: custom containerd config to
avoid Docker Desktop conflicts, socket permission management, and
CRI plugin disabling. ContainerdDaemon auto-detects WSL and writes
config before starting containerd.

Part of the containerd/nerdctl engine initiative.
Register lando-get-containerd-compat in index.js and
app-check-containerd-compat in app.js. Confirm engine.docker
proxy works with ContainerdContainer (same interface, no gap).

Part of the containerd/nerdctl engine initiative.
Populates lando.versions with containerd/nerdctl/buildkit versions
when running on the containerd backend.

Part of the containerd/nerdctl engine initiative.
Standalone bash script that exercises the full containerd engine path:
start containerd, start buildkitd, compose up nginx, verify, cleanup.

Part of the containerd/nerdctl engine initiative.
- Add explicit this.containerd property on ContainerdDaemon
- Add engine, binary paths, and supportedContainerdVersions to
  index.js defaults for discoverability
- Remove hardcoded fallback from Engine constructor

Part of the containerd/nerdctl engine initiative.
@netlify
Copy link
Copy Markdown

netlify bot commented Mar 14, 2026

Deploy Preview for lando-core ready!

Name Link
🔨 Latest commit dab5b91
🔍 Latest deploy log https://app.netlify.com/projects/lando-core/deploys/69c83109ece95000087a9c18
😎 Deploy Preview https://deploy-preview-445--lando-core.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 89 (🟢 up 2 from production)
Accessibility: 89 (no change from production)
Best Practices: 92 (no change from production)
SEO: 90 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@AaronFeledy AaronFeledy changed the title feat: containerd/nerdctl engine backend Experiment: containerd/nerdctl engine backend Mar 14, 2026
@AaronFeledy
Copy link
Copy Markdown
Member Author

@cursor review

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: Compose wrapper missing default for opts parameter
    • Added || {} default to datum.opts in both Docker and containerd compose wrappers to match original setup-engine.js behavior.
  • ✅ Fixed: Warning function ignores version info argument
    • Updated update-nerdctl-warning to accept and display version, update, and link parameters in the warning message.
  • ✅ Fixed: Unbounded recursion in container list retry logic
    • Added _retryCount parameter with a limit of 10 retries to prevent stack overflow in containerd-container list method.
  • ✅ Fixed: Containerd getVersions returns false causing semver.clean crash
    • Added filtering to remove false and 'skip' values from containerd versions before processing in getCompatibility.

Create PR

Or push these changes by commenting:

@cursor push abd2ef0b38
Preview (abd2ef0b38)
diff --git a/lib/backend-manager.js b/lib/backend-manager.js
--- a/lib/backend-manager.js
+++ b/lib/backend-manager.js
@@ -54,7 +54,7 @@
    * Returns a fully wired `Engine` instance ready for use by `lando.engine`.
    *
    * @param {string} [id='lando'] - The Lando instance identifier.
-   * @returns {Engine} A configured Engine instance.
+   * @return {Engine} A configured Engine instance.
    */
   createEngine(id = 'lando') {
     const engineType = this.config.engine || 'auto';
@@ -80,7 +80,7 @@
    * - Returns `new Engine(daemon, docker, compose, config)`
    *
    * @param {string} id - The Lando instance identifier.
-   * @returns {Engine} A Docker-backed Engine instance.
+   * @return {Engine} A Docker-backed Engine instance.
    * @private
    */
   _createDockerEngine(id) {
@@ -104,7 +104,7 @@
     );
 
     const compose = (cmd, datum) => {
-      const run = dockerCompose[cmd](datum.compose, datum.project, datum.opts);
+      const run = dockerCompose[cmd](datum.compose, datum.project, datum.opts || {});
       return this.shell.sh([orchestratorBin].concat(run.cmd), run.opts);
     };
 
@@ -124,7 +124,7 @@
    * `{cmd, opts}` shell descriptor, then executes via `shell.sh([nerdctlBin, ...cmd], opts)`.
    *
    * @param {string} id - The Lando instance identifier.
-   * @returns {Engine} A containerd-backed Engine instance.
+   * @return {Engine} A containerd-backed Engine instance.
    * @private
    */
   _createContainerdEngine(id) {
@@ -171,7 +171,7 @@
     // as the Docker path. Gets {cmd, opts} from NerdctlCompose, then executes
     // via shell.sh([nerdctlBin, ...cmd], opts).
     const compose = (cmd, datum) => {
-      const run = nerdctlCompose[cmd](datum.compose, datum.project, datum.opts);
+      const run = nerdctlCompose[cmd](datum.compose, datum.project, datum.opts || {});
       return this.shell.sh([nerdctlBin].concat(run.cmd), run.opts);
     };
 
@@ -193,7 +193,7 @@
    * Logs which engine was selected.
    *
    * @param {string} id - The Lando instance identifier.
-   * @returns {Engine} An Engine instance using the auto-detected backend.
+   * @return {Engine} An Engine instance using the auto-detected backend.
    * @private
    */
   _createAutoEngine(id) {

diff --git a/lib/backends/containerd/containerd-container.js b/lib/backends/containerd/containerd-container.js
--- a/lib/backends/containerd/containerd-container.js
+++ b/lib/backends/containerd/containerd-container.js
@@ -15,7 +15,7 @@
  * Helper to determine if any file exists in an array of files.
  *
  * @param {Array<string>} files - Array of file paths to check.
- * @returns {boolean}
+ * @return {boolean}
  * @private
  */
 const srcExists = (files = []) => _.reduce(files, (exists, file) => fs.existsSync(file) || exists, false);
@@ -33,7 +33,7 @@
  * - Labels whose values contain `,` within values that also contain `=`
  *
  * @param {string|Object} labels - Labels string from nerdctl or object from inspect.
- * @returns {Object} Docker-compatible labels object.
+ * @return {Object} Docker-compatible labels object.
  * @private
  */
 const parseLabels = labels => {
@@ -91,7 +91,7 @@
  * - `Status`   → status text
  *
  * @param {Object} nerdctlContainer - A parsed JSON line from `nerdctl ps --format json`.
- * @returns {Object} Docker API-compatible container object.
+ * @return {Object} Docker API-compatible container object.
  * @private
  */
 const normalizeContainer = nerdctlContainer => {
@@ -164,7 +164,7 @@
    * "No such container", "no such object", "not found".
    *
    * @param {Error} err - The error to inspect.
-   * @returns {boolean} `true` if the error indicates a missing resource.
+   * @return {boolean} `true` if the error indicates a missing resource.
    * @private
    */
   _isNotFoundError(err) {
@@ -184,7 +184,7 @@
    * @param {Array<string>} args - nerdctl subcommand and arguments.
    * @param {Object} [opts={}] - Additional options passed to `run-command`.
    * @param {boolean} [opts.ignoreReturnCode=false] - Whether to suppress non-zero exit errors.
-   * @returns {Promise<string>} The trimmed stdout from the command.
+   * @return {Promise<string>} The trimmed stdout from the command.
    * @throws {Error} If the command exits non-zero and `ignoreReturnCode` is false.
    * @private
    */
@@ -216,7 +216,7 @@
    *
    * @param {string} name - The name of the network to create.
    * @param {Object} [opts={}] - Additional network creation options.
-   * @returns {Promise<Object>} Network inspect data.
+   * @return {Promise<Object>} Network inspect data.
    */
   async createNet(name, opts = {}) {
     const args = ['network', 'create'];
@@ -254,7 +254,7 @@
    * Docker-compatible JSON.
    *
    * @param {string} cid - A container identifier (hash, name, or short id).
-   * @returns {Promise<Object>} Container inspect data.
+   * @return {Promise<Object>} Container inspect data.
    * @throws {Error} If the container does not exist.
    */
   async scan(cid) {
@@ -270,7 +270,7 @@
    * to prevent race conditions when containers are removed between checks.
    *
    * @param {string} cid - A container identifier.
-   * @returns {Promise<boolean>}
+   * @return {Promise<boolean>}
    */
   async isRunning(cid) {
     try {
@@ -303,9 +303,10 @@
    * @param {string}  [options.project] - Filter to a specific project name.
    * @param {Array<string>} [options.filter] - Additional `key=value` filters.
    * @param {string}  [separator='_'] - Container name separator.
-   * @returns {Promise<Array<Object>>} Array of Lando container descriptors.
+   * @param {number}  [_retryCount=0] - Internal retry counter to prevent unbounded recursion.
+   * @return {Promise<Array<Object>>} Array of Lando container descriptors.
    */
-  async list(options = {}, separator = '_') {
+  async list(options = {}, separator = '_', _retryCount = 0) {
     // Get raw container list from nerdctl (JSONL: one JSON object per line)
     let rawOutput;
     try {
@@ -380,7 +381,10 @@
     // If any container has been up for only a brief moment, retry
     // (matches Landerode behavior to avoid transient states)
     if (_.find(containers, container => container.status === 'Up Less than a second')) {
-      return this.list(options, separator);
+      if (_retryCount < 10) {
+        return this.list(options, separator, _retryCount + 1);
+      }
+      this.debug('list retry limit reached, proceeding with transient container states');
     }
 
     // Add running status flag
@@ -401,7 +405,7 @@
    * @param {Object} [opts={v: true, force: false}] - Removal options.
    * @param {boolean} [opts.v=true] - Also remove associated anonymous volumes.
    * @param {boolean} [opts.force=false] - Force-remove a running container.
-   * @returns {Promise<void>}
+   * @return {Promise<void>}
    */
   async remove(cid, opts = {v: true, force: false}) {
     const args = ['rm'];
@@ -428,7 +432,7 @@
    *
    * @param {string} cid - A container identifier.
    * @param {Object} [opts={}] - Stop options (e.g. `{t: 10}` for timeout in seconds).
-   * @returns {Promise<void>}
+   * @return {Promise<void>}
    */
   async stop(cid, opts = {}) {
     const args = ['stop'];
@@ -458,7 +462,7 @@
    * handle interface.
    *
    * @param {string} id - The network id or name.
-   * @returns {Object} A network handle with `inspect()` and `remove()` methods.
+   * @return {Object} A network handle with `inspect()` and `remove()` methods.
    */
   getNetwork(id) {
     return {
@@ -467,7 +471,7 @@
 
       /**
        * Inspect the network and return its metadata.
-       * @returns {Promise<Object>} Network inspect data.
+       * @return {Promise<Object>} Network inspect data.
        */
       inspect: async () => {
         const data = await this._nerdctl(['network', 'inspect', id]);
@@ -477,7 +481,7 @@
 
       /**
        * Remove the network.
-       * @returns {Promise<void>}
+       * @return {Promise<void>}
        */
       remove: async () => {
         try {
@@ -498,7 +502,7 @@
    *
    * @param {Object} [opts={}] - Filter options.
    * @param {Object} [opts.filters] - Filters object (e.g. `{name: ['mynet']}` or `{id: ['abc']}`).
-   * @returns {Promise<Array<Object>>} Array of network objects.
+   * @return {Promise<Array<Object>>} Array of network objects.
    */
   async listNetworks(opts = {}) {
     let rawOutput;
@@ -565,7 +569,7 @@
    * Dockerode Container handle interface.
    *
    * @param {string} cid - The container id or name.
-   * @returns {Object} A container handle with `inspect()`, `remove()`, and `stop()` methods.
+   * @return {Object} A container handle with `inspect()`, `remove()`, and `stop()` methods.
    */
   getContainer(cid) {
     return {
@@ -574,21 +578,21 @@
 
       /**
        * Inspect the container and return its metadata.
-       * @returns {Promise<Object>} Container inspect data.
+       * @return {Promise<Object>} Container inspect data.
        */
       inspect: () => this.scan(cid),
 
       /**
        * Remove the container.
        * @param {Object} [opts] - Removal options.
-       * @returns {Promise<void>}
+       * @return {Promise<void>}
        */
       remove: opts => this.remove(cid, opts),
 
       /**
        * Stop the container.
        * @param {Object} [opts] - Stop options.
-       * @returns {Promise<void>}
+       * @return {Promise<void>}
        */
       stop: opts => this.stop(cid, opts),
     };

diff --git a/lib/engine.js b/lib/engine.js
--- a/lib/engine.js
+++ b/lib/engine.js
@@ -194,7 +194,7 @@
     const semver = require('semver');
 
     // helper to normalize a supported versions object into comparison-ready format
-    const normalize = (sv) => _(sv)
+    const normalize = sv => _(sv)
       .map((data, name) => _.merge({}, data, {name}))
       .map(data => ([data.name, {
         satisfies: data.satisfies || `${data.min} - ${data.max}`,
@@ -207,12 +207,19 @@
 
     return this.daemon.getVersions().then(versions => {
       // Detect containerd backend: versions have containerd key instead of desktop/engine
-      const isContainerd = versions.hasOwnProperty('containerd');
+      const isContainerd = Object.prototype.hasOwnProperty.call(versions, 'containerd');
 
       let normalizedVersions;
       if (isContainerd) {
         // containerd format: {containerd, buildkit, nerdctl}
         normalizedVersions = normalize(this.supportedContainerdVersions);
+
+        // Remove false values (binaries that couldn't be versioned)
+        Object.keys(versions).forEach(key => {
+          if (versions[key] === false || versions[key] === 'skip') {
+            delete versions[key];
+          }
+        });
       } else {
         // Docker format: {desktop, engine, compose}
         normalizedVersions = normalize(supportedVersions);

diff --git a/messages/update-nerdctl-warning.js b/messages/update-nerdctl-warning.js
--- a/messages/update-nerdctl-warning.js
+++ b/messages/update-nerdctl-warning.js
@@ -1,13 +1,14 @@
 'use strict';
 
 // checks to see if a setting is disabled
-module.exports = () => ({
+module.exports = ({version, update, link} = {}) => ({
   type: 'warning',
   title: 'Recommend updating NERDCTL',
   detail: [
-    'Looks like you might be falling a bit behind on nerdctl.',
+    `You have version ${version || 'unknown'} but we recommend updating to ${update || 'the latest version'}.`,
     'In order to ensure the best stability and support we recommend you update',
     'by running the hidden "lando setup" command.',
   ],
   command: 'lando setup --skip-common-plugins',
+  url: link,
 });

cursoragent and others added 6 commits March 14, 2026 03:28
…ing, prevent recursion, filter false versions
New hook downloads Lima, creates a containerd-enabled VM during
'lando setup' on macOS. Platform-guarded in index.js.

Also fixes make-executable calls with absolute paths in both
darwin and containerd setup hooks, corrects Lima arch mapping
(arm64 → aarch64), and uses --plain for non-interactive VM creation.

Part of the containerd/nerdctl engine initiative.
Create utils/get-containerd-config.js for TOML config generation
on all platforms. Replaces WSL-specific config in wsl-helper.js.

Fixes: state/root as top-level scalars (not TOML tables),
disabled_plugins array for CRI, debug flag always-truthy bug.

17 new tests for config generation.

Part of the containerd/nerdctl engine initiative.
Create utils/get-buildkit-config.js for BuildKit TOML config generation.
Containerd worker with GC policy (reservedSpace), parallelism from CPU
count, optional registry mirrors and debug mode.

ContainerdDaemon now generates buildkit config, passes --config to
buildkitd, and exposes pruneBuildCache() via buildctl.

21 new tests. Config uses correct BuildKit field names per current docs.

Part of the containerd/nerdctl engine initiative.
- Add utils/setup-containerd-auth.js for Docker config path resolution,
  credential helper detection, and DOCKER_CONFIG env injection
- Update NerdctlCompose._transform() to inject auth env into commands
- Update ContainerdContainer._nerdctl() to pass auth env to nerdctl
- Add registryAuth config option for custom Docker config paths
- 23 new tests for auth config resolution and credential helpers
nerdctl v2 refuses to work as non-root even with --address pointing
to a rootful socket. Instead, use docker-compose (already installed
by Lando) with DOCKER_HOST pointing to finch-daemon's Docker API
socket. finch-daemon translates to containerd.

Architecture: docker-compose → finch-daemon → containerd (rootful)

- Replace NerdctlCompose with lib/compose.js (same as Docker path)
- Set DOCKER_HOST=unix://~/.lando/run/finch.sock in compose env
- Use existing docker-compose binary as orchestratorBin
- NerdctlCompose class retained but no longer used for compose ops
Move containerd/finch sockets from ~/.lando/run/ (user-controlled) to
/run/lando/ (root-owned) to prevent symlink attacks. Same pattern as
Docker using /var/run/docker.sock.

- Systemd RuntimeDirectory=lando creates /run/lando/ automatically
- ExecStartPost sets lando group permissions on sockets
- PID files stay in ~/.lando/run/ (user-level)
- Download finch-daemon binary during lando setup (root-owned)
- Add ExecStartPost to systemd service that launches finch-daemon
  alongside containerd, with socket at /run/lando/finch.sock
- Service task depends on finch-daemon being installed
… in status checks

- Replace daemon.up() with passive daemon.isUp() in hasRun() for build engine
  hooks (linux, darwin, win32) and landonet hook. These were triggering full
  retry loops with socket polling just to check installation status.
- WSL remains the exception: docker binaries only appear after Docker Desktop
  starts on Windows, so a minimal daemon.up({max:1}) is still needed there.
- Short-circuit containerd up() and _waitForSocket() early if required binaries
  (containerd, nerdctl) don't exist, avoiding futile retry loops on fresh installs.
- Fix double parse-setup-task() mutation: getSetupStatus() now extracts task
  fields with inline defaults instead of calling parse-setup-task(), which
  mutates/wraps the task object. setup() remains the sole caller.
The Docker build engine setup hooks run unconditionally, causing
lando setup to try installing Docker even when engine=containerd.
Add guard to skip Docker install on all platforms when containerd
is the selected engine.
nerdctl refuses to work as non-root with rootful containerd. Replace
all nerdctl shell-outs with Dockerode API calls via finch-daemon socket.

- ContainerdContainer now uses Dockerode({socketPath: finchSocket})
- list(), scan(), isRunning(), remove(), stop() use Docker API
- createNet(), listNetworks() use Docker API
- getContainer/getNetwork proxies wrap Dockerode objects
- No more nerdctl dependency for any runtime operation
- nerdctl binary no longer required (only kept for version checks)

Architecture: Dockerode → finch-daemon socket → containerd (rootful)
- Landonet setup now depends on setup-containerd-service when engine
  is containerd (was depending on setup-build-engine/Docker)
- Fix orchestratorBin to point to docker-compose (not nerdctl) when
  containerd engine is active
- Skip Docker Desktop binary check for containerd in landonet hasRun
The old service was 'enabled' but didn't have finch-daemon or /run/lando/
paths. hasRun returned true, skipping the service update. Now also
verifies both sockets exist at /run/lando/ before considering done.
When the service is already running with old config, 'systemctl start'
is a no-op. Need 'restart' to pick up the new service file with
finch-daemon and /run/lando/ socket paths.
finch-daemon adds unix:// internally. Passing unix:///run/lando/finch.sock
causes it to try 'unix://unix:///run/lando/finch.sock'. Just pass the
bare path.
finch-daemon/nerdctl needs /etc/cni/net.d/ for network lock files
and /opt/cni/bin/ for CNI plugins. Create them before containerd starts.
Users shouldn't need manual commands. Create /etc/cni/net.d and
/opt/cni/bin during the setup task with sudo, before restarting
the service.
finch-daemon uses nerdctl internally which defaults to /run/containerd/
containerd.sock. Need to point it at our socket /run/lando/containerd.sock
via CONTAINERD_ADDRESS env var.
finch-daemon internally uses nerdctl which looks for containerd at
/run/containerd/containerd.sock. Symlink our socket there so
finch-daemon finds it without custom config.
isUp() and _healthCheck() used nerdctl ps which fails with rootless
error. Replace with Dockerode.ping() against finch-daemon socket —
same pattern as all other container ops.
Remove the /run/containerd/containerd.sock symlink hack. Instead,
write a nerdctl.toml config pointing to our socket and set NERDCTL_TOML
env var when starting finch-daemon. No conflict with system containerd.
finch-daemon expects CNI plugins at /opt/cni/bin/ but Ubuntu installs
them at /usr/lib/cni/. Symlink in ExecStartPre. Also add cni_path to
nerdctl.toml config.
…pping

docker-compose on WSL detects Docker Desktop and translates bind mount
paths through /run/desktop/mnt/host/wsl/docker-desktop-bind-mounts/.
Set DOCKER_CONTEXT=default to prevent this when using containerd.
containerd needs runc in PATH. Our runc is at /usr/local/lib/lando/bin/.
Add Environment=PATH with our bin dir to the systemd service.
…ll failing tests

Task 32: Enforce BRIEF directive — never shell out to nerdctl from user-facing code.

Source changes:
- hooks/lando-doctor-containerd.js: Remove nerdctl binary check, add
  docker-compose check with PATH-aware lookup
- hooks/app-check-containerd-compat.js: Replace nerdctl compose shellout
  with docker-compose + DOCKER_HOST check via finch-daemon
- hooks/app-add-2-landonet.js: Use lando.engine.docker.dockerode instead
  of creating standalone Docker instances against finch socket
- messages/nerdctl-not-found.js -> containerd-binaries-not-found.js
- messages/nerdctl-compose-failed.js -> compose-failed-containerd.js
- messages/update-nerdctl-warning.js -> update-containerd-warning.js
  (rewritten to use component name dynamically)

Test fixes:
- app-add-2-landonet: Mock Dockerode exec API instead of shell.sh
- containerd-integration: down() is a no-op on Linux per BRIEF design
- docker-engine: Replace nonexistent _getContainerdNerdctlLoadCommand test
  with _loadContainerdImageIntoFinch (Dockerode-based loading)
- setup-containerd-auth: Use temp dirs with controlled configs instead of
  reading real ~/.docker/config.json (credsStore sanitization)
- backend-manager: Expect docker-compose as compose binary, not nerdctl
- containerd-messages: Update references to renamed message files

All 510 tests passing, 0 failures.
…kend (Task 28)

- Create proxy-adapter.js for CNI network pre-creation before proxy start
- Remove containerd early-return in app-add-proxy-2-landonet.js; the
  ContainerdContainer.getNetwork() Dockerode-compatible interface makes
  bridge network connect/disconnect work for both backends
- Add CNI network ensurance in app-start-proxy.js for proxy edge network
  and app-level proxy network references
- Export ContainerdProxyAdapter from containerd barrel index
- Add docs/dev/containerd-proxy-design.md documenting the architecture
- Add 12 unit tests covering proxy-adapter and hook changes

The socket mapping was already in place: lando-set-proxy-config.js sets
dockerSocket to finch-daemon path, and _proxy builder mounts it as
/var/run/docker.sock inside the Traefik container. finch-daemon provides
Docker API v1.43 which Traefik's Docker provider consumes transparently.
…ting docs (Tasks 30, 33)

Move CNI directories from system-wide /etc/cni/net.d/finch and /usr/lib/cni
to Lando-isolated /etc/lando/cni/finch and /usr/local/lib/lando/cni/bin to
avoid conflicts with other container runtimes.

lando setup now downloads CNI plugins, sets chgrp/chmod on the CNI conf
directory so ensureCniNetwork() works from user-land without sudo, and
enforces permissions on every systemd service start via ExecStartPre.

Refactor elevated-access prompting into a shared ensurePassword() helper
with an upfront hidden auth task so the user is prompted once before
downloads begin. Filter hidden tasks from setup status output.

Add docs/troubleshooting/containerd.md covering all 10 error scenarios and
update 7 message modules to link to specific troubleshooting sections.

Also: fix doctor hook to resolve binaries from systemBinDir, verify
containerd binary exists in Engine.dockerInstalled, add lando doctor CLI
task, and add tests for CNI permissions, nerdctl config, and missing binary
detection.
… (Task 34)

Previously the compose wrapper only created a CNI conflist for the
implicit _default network. Custom networks defined in compose files
(e.g. frontend, backend, proxy edge) had no CNI configs, causing
container networking failures when the nerdctl OCI hook couldn't
find them.

New ensure-compose-cni-networks utility parses compose YAML files,
resolves docker-compose-style network names, and pre-creates CNI
conflist files for every non-external network before docker-compose up.
…dead code (Task 35)

- Fix binary path bug in lando-setup-containerd-engine-check.js: was checking
  ~/.lando/bin/ for containerd and buildkitd, but they are installed to
  /usr/local/lib/lando/bin/ (system binaries). Only nerdctl lives in ~/.lando/bin/.
- Add 23 unit tests for ensure-cni-network.js covering conflist creation, subnet
  allocation, error handling, and debug logging.
- Extend finch-daemon-manager.spec.js from 18 to 34 tests adding lifecycle
  coverage for _isProcessRunning, start, stop, isRunning, and _cleanup.
- Deprecate unused NerdctlCompose and setup-engine-containerd.js (production uses
  docker-compose + DOCKER_HOST via BackendManager._createContainerdEngine).
- Remove NerdctlCompose from containerd index.js public exports.
- Update todo.md and BRIEF.md with remaining work and gotchas.
…test to production path (Task 36)

- Add 60 tests for LimaManager covering constructor, vmExists, createVM,
  startVM, stopVM, isRunning, getSocketPath, exec, nerdctl, _parseListOutput
- Add 19 tests for WslHelper covering isWsl, isDockerDesktopRunning,
  ensureSocketPermissions
- Rewrite smoke test script to use docker-compose + DOCKER_HOST + finch-daemon
  instead of deprecated nerdctl compose path
- Update todo.md and BRIEF.md with task completion status
… deadlock, and add e2e tests (Tasks 37-40)

Three-part fix for container outbound internet connectivity:
- Replace CNI plugin chain [bridge, firewall, tc-redirect-tap] with
  [bridge, portmap, firewall, tuning]; tc-redirect-tap is not installed
  by lando setup and is only needed for VM-based runtimes
- Add ip_forward sysctl and LANDO-FORWARD iptables chain to systemd
  ExecStartPre for belt-and-suspenders outbound traffic forwarding
- Auto-migrate stale conflist files in-place preserving subnet/bridge/ID

Also includes uncommitted work from prior sessions (Tasks 37-39):
- Move containerd state dir to /run/lando/containerd (ephemeral tmpfs)
- Fix OCI hook deadlock via NERDCTL_TOML env var in systemd service
- Fix binary path check to use system bin dir
- Add Leia e2e test, CI workflow, and 44 compose integration tests
- Add nerdctl config tests for CNI path isolation

722 tests passing, 0 failing.
…work labels and portmap (Task 41)

Fix two finch-daemon issues blocking multi-container lando start:

1. Network label loss: finch-daemon doesn't persist com.docker.compose.*
   labels across restarts, causing docker-compose v2 to reject existing
   networks. Added removeStaleComposeNetworks() and
   removeComposeCniConflists() to clean up before compose up.

2. CNI portmap rejects HostPort:0: nerdctl OCI hook passes random port
   mappings (HostPort:0) directly to portmap plugin, which rejects them.
   Removed portmap from CNI plugin chain (Lando proxy handles port
   routing) and split compose up into two phases (--no-start + conflist
   overwrite + start) so finch-daemon's conflist can be corrected.

Added 15 multi-container tests covering landonet host injection,
compose CNI integration, and Leia E2E verification for both services.
737 tests passing, 0 failing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants